第 3 章 Spring Boot的Web 应用支持
本章重点:Web 开发是 Spring Boot 最核心的应用场景。本章从 Java Web 三大组件、Spring MVC 定制、文件上传到异常处理,全面讲解 Spring Boot 的 Web 开发能力。学完本章,你就能独立完成一个 Web 项目的基础搭建。
3.1 Java Web 三大组件注册
3.1.1 三大组件简介
Java Web 三大组件指的是 Servlet、Filter、Listener,这是 Java Web 开发的基础。
| 组件 | 作用 | 典型应用 |
|---|---|---|
| Servlet | 处理 HTTP 请求,生成响应 | 动态页面、接口处理 |
| Filter | 拦截请求,在请求前后做处理 | 编码过滤、登录校验、权限检查 |
| Listener | 监听 Web 应用的事件 | 统计在线人数、初始化资源 |
衔接知识:你们在 Java Web 课程里学过的 Servlet、Filter、Listener,在 Spring Boot 里依然能用。Spring Boot 内嵌了 Tomcat,所以这些原生 Web 组件照样可以注册使用。
为什么还要用原生组件?
虽然 Spring MVC 已经很强大了,但有时候还是需要用到原生 Web 组件:
- 老项目迁移到 Spring Boot,原来的 Servlet/Filter 还能用
- 某些第三方库依赖原生 Servlet API
- 一些特殊场景(比如非常底层的过滤逻辑)用 Filter 更直接
Spring Boot 提供了三种注册方式,下面逐一讲解。
3.1.2 方式一:Spring Bean 注册(了解)
最简单的方式——直接把 Servlet、Filter、Listener 作为 Bean 注册到 Spring 容器,Spring Boot 会自动识别并注册到 Web 服务器。
示例代码
自定义 Servlet:
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("hello FirstServlet");
resp.getWriter().write("hello FirstServlet");
}
}自定义 Filter:
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest requ, ServletResponse resp, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("处理请求前的处理");
filterChain.doFilter(requ, resp);
System.out.println("处理请求后的处理");
}
}自定义 Listener:
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("----Web应用初始化完成----");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("----Web应用销毁之前----");
}
}配置类注册:
@Configuration
public class WebConfigure {
@Bean
public FirstServlet firstServlet() {
return new FirstServlet();
}
@Bean
public SecondServlet secondServlet() {
return new SecondServlet();
}
@Bean
public MyFilter myFilter() {
return new MyFilter();
}
@Bean
public MyListener myListener() {
return new MyListener();
}
}映射规则
- Servlet:映射路径 = Bean名称 +
/,比如 Bean 名叫firstServlet,访问路径就是/firstServlet/ - Filter:默认映射路径是
/*(拦截所有请求) - Listener:自动注册,不需要映射路径
缺点
- Servlet 的映射路径由 Bean 名称决定,不够灵活
- Filter 默认拦截所有请求,不能自定义拦截路径
- 只有一个 Servlet 时,Bean 名称作为路径可能不符合预期
适用场景:简单场景、快速开发、对映射路径要求不高的情况。一般不推荐。
3.1.3 方式二:RegistrationBean 注册(推荐)
Spring Boot 提供了三个 RegistrationBean 来注册三大组件,可以灵活控制映射路径、顺序等参数。
| 组件 | 对应的 RegistrationBean |
|---|---|
| Servlet | ServletRegistrationBean |
| Filter | FilterRegistrationBean |
| Listener | ServletListenerRegistrationBean |
示例代码
@Configuration
public class WebConfigure {
// 注册 Servlet
@Bean
public ServletRegistrationBean<FirstServlet> firstServlet() {
return new ServletRegistrationBean<>(new FirstServlet(), "/first");
}
@Bean
public ServletRegistrationBean<SecondServlet> secondServlet() {
return new ServletRegistrationBean<>(new SecondServlet(), "/second");
}
// 注册 Filter
@Bean
public FilterRegistrationBean<MyFilter> myFilter() {
FilterRegistrationBean<MyFilter> filterBean = new FilterRegistrationBean<>();
filterBean.setFilter(new MyFilter());
filterBean.setUrlPatterns(Arrays.asList("/first")); // 只拦截 /first
filterBean.setOrder(1); // 过滤器执行顺序,数字越小越先执行
return filterBean;
}
// 注册 Listener
@Bean
public ServletListenerRegistrationBean<MyListener> myListener() {
return new ServletListenerRegistrationBean<>(new MyListener());
}
}常用配置
ServletRegistrationBean 常用方法:
| 方法 | 说明 |
|---|---|
setServlet() | 设置 Servlet 实例 |
addUrlMappings() / 构造方法 | 设置映射路径 |
setLoadOnStartup() | 设置启动加载顺序 |
setInitParameters() | 设置初始化参数 |
setName() | 设置 Servlet 名称 |
FilterRegistrationBean 常用方法:
| 方法 | 说明 |
|---|---|
setFilter() | 设置 Filter 实例 |
setUrlPatterns() | 设置拦截路径 |
setOrder() | 设置执行顺序(数字越小越先执行) |
setInitParameters() | 设置初始化参数 |
setServletRegistrationBeans() | 指定拦截哪些 Servlet |
企业最佳实践:推荐用 RegistrationBean 方式注册,灵活可控。尤其是 Filter,一定要指定拦截路径,不要默认拦截所有,避免影响性能。
3.1.4 方式三:注解扫描注册
用 Servlet 3.0 提供的注解 @WebServlet、@WebFilter、@WebListener 标注组件,然后在启动类上加 @ServletComponentScan 扫描。
示例代码
Servlet:
@WebServlet("/first")
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("hello FirstServlet");
}
}Filter:
@WebFilter("/first")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest requ, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
System.out.println("处理请求前");
chain.doFilter(requ, resp);
System.out.println("处理请求后");
}
}Listener:
@WebListener
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("Web应用初始化");
}
}启动类添加扫描注解:
@SpringBootApplication
@ServletComponentScan // 扫描 @WebServlet、@WebFilter、@WebListener
public class Chapter03Application {
public static void main(String[] args) {
SpringApplication.run(Chapter03Application.class, args);
}
}说明
@ServletComponentScan默认扫描启动类所在包及其子包- 可以指定扫描的包:
@ServletComponentScan("com.itheima.web") - 不需要额外写配置类,代码更简洁
注意:Spring Boot 默认不会扫描
@WebServlet、@WebFilter、@WebListener注解,必须加@ServletComponentScan才会生效。这是新手容易踩的坑——加了注解但忘了加扫描注解,组件不生效。
3.1.5 三种方式对比与选型
| 对比项 | Spring Bean 注册 | RegistrationBean | 注解扫描 |
|---|---|---|---|
| 灵活性 | 低 | 高 | 中 |
| 映射路径控制 | 弱(Bean名决定) | 强(完全自定义) | 中(注解指定) |
| 代码量 | 少 | 较多 | 少 |
| 是否需要配置类 | 是 | 是 | 否(启动类加注解) |
| 顺序控制 | 不支持 | 支持(setOrder) | 支持(@Order) |
| 初始化参数 | 不支持 | 支持 | 支持(注解属性) |
| 适用场景 | 简单测试 | 生产环境(推荐) | 中小项目 |
选型建议:
- 生产环境推荐:RegistrationBean 方式,最灵活可控
- 简单项目/快速开发:注解扫描方式,代码简洁
- Spring Bean 方式:了解即可,实际项目用得少
小贴士:
- 实际开发中,大部分场景用 Spring MVC 的 Controller 就够了,很少需要直接写 Servlet
- Filter 用得相对多一些,比如做登录校验、跨域处理、日志记录等
- Listener 用得最少,一般在应用启动时做初始化才会用到
- 面试可能会问"Spring Boot 怎么注册 Servlet/Filter/Listener",三种方式都要能说出来
3.2 Spring MVC 自动配置与定制
3.2.1 Spring MVC 自动配置特性
Spring Boot 引入 spring-boot-starter-web 后,会自动配置 Spring MVC,开箱即用。自动配置主要做了这些事:
- 视图解析器:自动注册
ContentNegotiatingViewResolver和BeanNameViewResolver - 静态资源支持:静态资源、WebJars 自动映射
- 转换器和格式化器:自动注册
Converter、Formatter - 消息转换器:
HttpMessageConverters,支持 JSON、XML 等 - 首页支持:自动映射
index.html - 网站图标:自动加载
favicon.ico - 数据绑定器:
ConfigurableWebBindingInitializer自动初始化
一句话总结:Spring Boot 把 Spring MVC 常用的配置都自动配好了,你直接用就行。
面试常问:Spring Boot 自动配置了 Spring MVC 的哪些东西? 答:视图解析器、静态资源映射、转换器/格式化器、消息转换器、首页支持、网站图标、数据绑定器等。
3.2.2 静态资源映射
默认静态资源位置
Spring Boot 默认会从以下 5 个位置查找静态资源,优先级从高到低:
| 优先级 | 路径 | 说明 |
|---|---|---|
| 1(最高) | classpath:/META-INF/resources/ | WebJars 等第三方资源 |
| 2 | classpath:/resources/ | 资源文件 |
| 3 | classpath:/static/ | 静态资源(最常用) |
| 4(最低) | classpath:/public/ | 公共资源 |
注意:这 5 个目录都是在
src/main/resources下面的。一般项目都用static目录放静态资源。
访问方式
把静态资源放到 static 目录下,直接通过 URL 访问:
src/main/resources/static/
├── css/
│ └── style.css
├── js/
│ └── app.js
├── images/
│ └── logo.png
└── index.html访问地址:
http://localhost:8080/index.htmlhttp://localhost:8080/css/style.csshttp://localhost:8080/images/logo.png
不需要加 /static/ 前缀,直接访问文件名就行。
自定义静态资源映射
默认的 5 个位置不够用?比如你想把文件存在 backend 目录下,或者存在磁盘的某个目录下,可以自定义映射。
方式一:配置类方式(推荐)
实现 WebMvcConfigurer 接口,重写 addResourceHandlers 方法:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// /backend/** 开头的请求,去 classpath:/backend/ 目录找资源
registry.addResourceHandler("/backend/**")
.addResourceLocations("classpath:/backend/");
// 也可以映射到磁盘的绝对路径
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:D:/upload/");
}
}方式二:配置文件方式
spring:
mvc:
static-path-pattern: /backend/** # 静态资源的访问路径模式
web:
resources:
static-locations: classpath:/backend/ # 静态资源存放的目录两种方式的区别:
- 配置文件方式:修改默认的静态资源路径,会覆盖默认的 5 个位置
- 配置类方式:在默认的基础上新增映射,不影响默认的
- 推荐用配置类方式,更灵活,可以加多个映射
WebJars 支持
WebJars 是把前端资源(jQuery、Bootstrap 等)打成 JAR 包,通过 Maven 引入使用。
引入依赖:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>访问方式:
http://localhost:8080/webjars/jquery/3.6.0/jquery.jsSpring Boot 自动映射了 /webjars/** 到 classpath:/META-INF/resources/webjars/。
企业实践:现在前后端分离项目一般不用 WebJars,前端资源由前端项目管理。但传统的服务端渲染项目或者一些简单的后台管理系统可能会用到。
3.2.3 视图控制器(ViewController)
有些页面跳转不需要业务逻辑,只是简单的页面跳转。如果每个都写一个 Controller 方法,很麻烦。
传统写法:
@Controller
public class PageController {
@GetMapping("/toLogin")
public String toLogin() {
return "login"; // 跳转到登录页
}
@GetMapping("/toRegister")
public String toRegister() {
return "register"; // 跳转到注册页
}
}用视图控制器简化:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 访问 /backend/toLoginPage,跳转到 /backend/login.html
registry.addViewController("/backend/toLoginPage")
.setViewName("/backend/login.html");
// 访问 /backend,跳转到 /backend/index.html
registry.addViewController("/backend")
.setViewName("/backend/index.html");
}
}这样就不用写 Controller 了,直接配置映射关系就行。
适用场景:没有业务逻辑的纯页面跳转。如果跳转前需要处理数据,还是得写 Controller。
3.2.4 拦截器(Interceptor)
拦截器是 Spring MVC 提供的,用于在请求处理前后做一些处理。和 Filter 类似,但更精细,可以拦截到具体的 Controller 方法。
拦截器 vs Filter
| 对比项 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 规范 | Servlet 规范 | Spring MVC 规范 |
| 拦截时机 | 请求到达 Servlet 之前 | 请求到达 Controller 之前 |
| 拦截粒度 | URL 级别 | 方法级别(更细) |
| 能否访问 Spring 容器 | 不能(早期执行) | 能(Spring 管理) |
| 能否获取方法信息 | 不能 | 能(HandlerMethod) |
| 执行顺序 | Filter → Interceptor → Controller |
自定义拦截器
实现 HandlerInterceptor 接口,有三个方法:
public class LoginInterceptor implements HandlerInterceptor {
// 请求处理之前调用(Controller 方法执行前)
// 返回 true 放行,返回 false 拦截
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 检查用户是否登录
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) {
// 未登录,跳转到登录页
request.getRequestDispatcher("/backend/login.html")
.forward(request, response);
return false; // 拦截
}
return true; // 放行
}
// 请求处理之后调用(Controller 方法执行后,视图渲染前)
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// 可以修改 ModelAndView
}
// 整个请求完成后调用(视图渲染后)
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 清理资源、记录日志等
}
}注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns( // 放行这些路径
"/backend/login.html", // 登录页
"/backend/login", // 登录接口
"/css/**", // 静态资源
"/js/**",
"/images/**"
);
}
}常用配置
| 方法 | 说明 |
|---|---|
addPathPatterns() | 添加拦截的路径 |
excludePathPatterns() | 排除不拦截的路径 |
order() | 拦截器执行顺序,数字越小越先执行 |
注意:拦截器默认会拦截静态资源!所以一定要把静态资源路径排除掉,否则 CSS、JS、图片都会被拦截,页面样式乱掉。
企业最佳实践:
- 登录校验、权限验证用拦截器
- 多个拦截器按顺序执行,注意 order 的设置
- 拦截器里不要写复杂的业务逻辑,保持轻量
- 前后端分离项目中,拦截器常用来校验 Token
3.2.5 WebMvcConfigurer 常用配置总结
WebMvcConfigurer 接口提供了很多方法,可以定制 Spring MVC 的各种配置:
| 方法 | 作用 |
|---|---|
addResourceHandlers() | 静态资源映射 |
addViewControllers() | 视图控制器(页面跳转) |
addInterceptors() | 拦截器 |
configureMessageConverters() | 消息转换器(JSON/XML等) |
addFormatters() | 格式化器(日期、数字等) |
addCorsMappings() | 跨域配置 |
configureContentNegotiation() | 内容协商配置 |
extendMessageConverters() | 扩展消息转换器 |
记住常用的几个就行:静态资源、视图控制器、拦截器、跨域。其他的用到再查。
3.2.6 @EnableWebMvc 完全接管 MVC
默认情况下,Spring Boot 自动配置 + 你自定义的配置是并存的——自动配置保留,你的配置追加进去。
如果你加了 @EnableWebMvc 注解,Spring Boot 的 MVC 自动配置就会全部失效,完全由你自己管理。
@Configuration
@EnableWebMvc // 关闭 Spring Boot 的 MVC 自动配置,完全自己控制
public class WebMvcConfig implements WebMvcConfigurer {
// 所有配置都要自己写
}重要提醒:
- 一般不要加
@EnableWebMvc!加了之后所有自动配置都失效了,包括静态资源映射、消息转换器等,你得全部自己配- 只有在需要完全定制 MVC 配置的特殊场景才用
- 新手最容易犯的错:看教程加了
@EnableWebMvc,结果静态资源访问不了、JSON 返回不对,排查半天- 记住:99% 的场景用
WebMvcConfigurer就够了,不需要@EnableWebMvc
3.3 文件上传与下载
3.3.1 文件上传自动配置原理
Spring Boot 对文件上传做了自动配置,由 MultipartAutoConfiguration 类实现:
- 自动配置了
StandardServletMultipartResolver(基于 Servlet 3.0 的文件上传) - 不需要引入第三方 jar 包(比如 commons-fileupload)
- 配置属性以
spring.servlet.multipart开头
常用配置:
spring:
servlet:
multipart:
enabled: true # 启用文件上传(默认 true)
max-file-size: 10MB # 单个文件最大大小
max-request-size: 50MB # 整个请求最大大小(多文件上传时)
file-size-threshold: 0 # 超过这个大小就写入临时文件(单位字节)
location: # 临时文件目录注意:默认单个文件最大 1MB,整个请求最大 10MB。上传大文件一定要改配置,否则会报错。
3.3.2 单文件上传
Controller 代码:
@RestController
public class FileController {
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
// 1. 判断文件是否为空
if (file.isEmpty()) {
return new Result(false, "请选择文件");
}
// 2. 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 3. 生成新文件名(防止重名覆盖)
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID() + suffix;
// 4. 指定上传目录
String uploadDir = "src/main/resources/static/upload/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs(); // 目录不存在就创建
}
// 5. 保存文件
try {
file.transferTo(new File(uploadDir + newFilename));
return new Result(true, "上传成功", "/upload/" + newFilename);
} catch (IOException e) {
e.printStackTrace();
return new Result(false, "上传失败");
}
}
}MultipartFile 常用方法:
| 方法 | 说明 |
|---|---|
isEmpty() | 文件是否为空 |
getOriginalFilename() | 获取原始文件名 |
getSize() | 获取文件大小(字节) |
getContentType() | 获取文件类型 |
getBytes() | 获取文件字节数组 |
getInputStream() | 获取文件输入流 |
transferTo(File) | 保存文件到指定位置 |
3.3.3 多文件上传
多文件上传用 MultipartFile[] 数组:
@PostMapping("/multiUpload")
public Result multiUpload(@RequestParam("files") MultipartFile[] files) {
List<String> fileUrls = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
continue;
}
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID() + suffix;
String uploadDir = "src/main/resources/static/upload/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
try {
file.transferTo(new File(uploadDir + newFilename));
fileUrls.add("/upload/" + newFilename);
} catch (IOException e) {
e.printStackTrace();
return new Result(false, "上传失败");
}
}
return new Result(true, "上传成功", fileUrls);
}3.3.4 文件下载
@GetMapping("/download")
public void download(@RequestParam String filename, HttpServletResponse response) {
String filePath = "src/main/resources/static/upload/" + filename;
File file = new File(filePath);
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType("application/octet-stream");
response.setContentLength((int) file.length());
try {
// 设置下载文件名(处理中文文件名)
String downloadName = URLEncoder.encode(filename, "UTF-8");
response.setHeader("Content-Disposition",
"attachment;filename=" + downloadName);
// 写入响应
Files.copy(file.toPath(), response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}3.3.5 企业级实践
1. 文件存储方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地磁盘 | 简单、免费 | 单机存储、扩容难、丢失风险 | 开发测试、小项目 |
| NFS 共享存储 | 多机共享 | 性能一般、单点故障 | 中小项目 |
| 对象存储(OSS/COS/七牛) | 海量存储、高可用、CDN加速 | 收费 | 生产环境(推荐) |
企业现状:生产环境基本都用对象存储(阿里云 OSS、腾讯云 COS、七牛云等),不会存在本地。本地存储只在开发测试用。
2. 文件命名策略
- 不要用原始文件名:可能重名覆盖,可能有特殊字符
- 推荐:UUID + 后缀,或者日期路径 + UUID
- 示例:
2024/01/15/uuid.jpg,按日期分目录,便于管理
3. 安全校验
- 校验文件类型:不能只看后缀名,要校验文件头(Magic Number)
- 校验文件大小:防止超大文件攻击
- 文件名处理:过滤特殊字符,防止路径遍历攻击
- 上传目录隔离:上传目录不能在可执行目录下
4. 其他注意事项
- 上传的文件不要放在
resources目录下,打包后就写不进去了 - 生产环境要配置独立的上传目录,最好在项目外面
- 大文件上传要考虑分片上传
- 要有文件清理策略,防止磁盘占满
小贴士:
- 面试可能会问"文件上传怎么实现的",要能说出 MultipartFile、配置、注意事项
- 生产环境用对象存储,这是标配,提前了解一下阿里云 OSS 的使用
- 文件上传是安全漏洞的高发区,一定要做校验,不能什么文件都让传
3.4 异常处理
3.4.1 Spring Boot 默认异常处理机制
Spring Boot 提供了默认的异常处理机制,由 ErrorMvcAutoConfiguration 自动配置类实现。
四大核心组件
| 组件 | 作用 |
|---|---|
ErrorPageCustomizer | 错误页面定制,发生异常后转发到 /error |
BasicErrorController | 错误控制器,处理 /error 请求 |
DefaultErrorViewResolver | 默认错误视图解析器 |
DefaultErrorAttributes | 错误属性处理,封装错误信息 |
异常处理流程
- 程序抛出异常
ErrorPageCustomizer将请求转发到/errorBasicErrorController处理/error请求- 根据请求类型返回不同结果:
- 浏览器请求(
Accept: text/html)→ 返回错误页面(HTML) - 其他请求(如 Ajax)→ 返回 JSON 数据
- 浏览器请求(
DefaultErrorViewResolver解析错误视图DefaultErrorAttributes封装错误信息
默认错误信息
默认返回的错误信息包含这些字段:
| 字段 | 说明 |
|---|---|
timestamp | 时间戳 |
status | HTTP 状态码 |
error | 错误描述 |
exception | 异常类名 |
message | 错误消息 |
trace | 异常栈信息 |
path | 请求路径 |
了解原理即可:实际开发中我们一般不用默认的,都会自定义异常处理。
3.4.2 自定义错误页面
如果只是想自定义错误页面的样式,很简单:
在 templates/error/ 目录下创建对应状态码的 HTML 页面:
src/main/resources/templates/error/
├── 404.html ← 404 错误页面
├── 4xx.html ← 4xx 系列错误页面(404 没有的话用这个)
├── 500.html ← 500 错误页面
└── 5xx.html ← 5xx 系列错误页面或者在静态资源目录的 error/ 下:
src/main/resources/static/error/
└── 404.htmlSpring Boot 会自动查找这些页面,精确匹配优先(404.html 优先于 4xx.html)。
3.4.3 全局异常处理(推荐)
用 @ControllerAdvice + @ExceptionHandler 做全局异常处理,这是企业里最常用的方式。
基本用法
@ControllerAdvice
public class MyExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(MyException.class)
@ResponseBody
public Result handleMyException(MyException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
}
// 处理系统异常
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handleException(Exception e) {
e.printStackTrace();
return new Result(false, "系统繁忙,请稍后重试");
}
}如果是 @RestControllerAdvice,就不用加 @ResponseBody 了,默认都返回 JSON:
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(MyException.class)
public Result handleMyException(MyException e) {
return new Result(false, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
e.printStackTrace();
return new Result(false, "系统错误");
}
}工作原理
@ControllerAdvice:控制器增强,全局拦截 Controller 的异常@ExceptionHandler:指定处理哪种异常- 异常匹配规则:优先匹配精确的异常类型,没有的话匹配父类
- 可以有多个
@ExceptionHandler方法,分别处理不同异常
3.4.4 自定义业务异常
企业开发中,通常会定义自己的业务异常类,用来区分业务异常和系统异常。
自定义异常类:
public class MyException extends RuntimeException {
private String code; // 错误码
public MyException(String message) {
super(message);
}
public MyException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}错误码枚举(更规范的写法):
public enum ErrorEnum {
// 400 系列
BAD_REQUEST("400", "请求的数据格式不符!"),
UNAUTHORIZED("401", "登录凭证过期!"),
FORBIDDEN("403", "抱歉,你无权限访问!"),
NOT_FOUND("404", "请求的资源找不到!"),
// 500 系列
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
SERVICE_UNAVAILABLE("503", "服务器正忙,请稍后再试!"),
// 业务异常
USER_NOT_EXIST("10001", "用户不存在"),
PASSWORD_ERROR("10002", "密码错误"),
PARAM_ERROR("10003", "参数错误");
private final String code;
private final String msg;
ErrorEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() { return code; }
public String getMsg() { return msg; }
}使用方式:
@RestController
@RequestMapping("/dish")
public class DishController {
@GetMapping("/del/{id}")
public Result delDish(@PathVariable String id) {
Integer dishId = Integer.valueOf(id);
if (dishId < 0) {
// 抛出业务异常
throw new MyException(ErrorEnum.PARAM_ERROR.getMsg());
}
return new Result(true, "删除成功");
}
}3.4.5 统一返回结果与异常处理结合
企业项目中,通常会定义统一的返回结果格式,异常处理也返回统一格式。
统一返回结果类:
@Data
public class Result<T> implements Serializable {
private boolean flag; // 是否成功
private String message; // 提示信息
private T data; // 返回数据
public Result(boolean flag, String message) {
this.flag = flag;
this.message = message;
}
public Result(boolean flag, String message, T data) {
this.flag = flag;
this.message = message;
this.data = data;
}
// 快捷方法
public static <T> Result<T> success(String message, T data) {
return new Result<>(true, message, data);
}
public static <T> Result<T> success(T data) {
return new Result<>(true, "操作成功", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(false, message);
}
}全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(MyException.class)
public Result handleBusinessException(MyException e) {
// 业务异常,返回具体的错误信息
return Result.error(e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(message);
}
// 处理系统异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
// 系统异常,打日志,返回通用提示
e.printStackTrace(); // 生产环境用日志框架
return Result.error("系统繁忙,请稍后重试");
}
}3.4.6 企业级异常处理最佳实践
分层处理异常:
- 业务异常(自定义异常):返回具体错误信息,用户能看懂
- 系统异常(Exception):返回通用提示,不暴露内部细节
- 参数校验异常:返回校验失败的具体字段和原因
异常信息要打日志:
- 业务异常可以不打(预期内的)
- 系统异常一定要打 error 级别日志,方便排查
不要捕获异常后不处理:
- 最忌讳
catch (Exception e) {},吞掉异常不处理 - 要么处理,要么往上抛,不要静默失败
- 最忌讳
异常粒度要合适:
- 不要什么都抛 RuntimeException
- 不同业务场景定义不同的异常类型,便于区分处理
不要用异常控制业务流程:
- 异常是用来处理异常情况的,不是用来做正常流程控制的
- 比如"用户不存在"这种预期内的情况,可以返回错误码,不一定非要抛异常
面试常问:Spring Boot 怎么做全局异常处理? 答:用 @RestControllerAdvice + @ExceptionHandler,定义全局异常处理类,分别处理不同类型的异常,返回统一格式的结果。
3.5 Web 开发进阶(企业级扩展)
3.5.1 跨域处理(CORS)
什么是跨域
浏览器的同源策略:协议、域名、端口都相同才叫同源,不同源的请求会被浏览器拦截。
跨域是前后端分离项目必遇到的问题——前端在 http://localhost:8080,后端在 http://localhost:9090,端口不同,就是跨域。
Spring Boot 解决跨域
方式一:全局配置(推荐)
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许所有路径
.allowedOriginPatterns("*") // 允许所有来源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许的请求头
.allowCredentials(true) // 允许携带 Cookie
.maxAge(3600); // 预检请求缓存时间(秒)
}
}方式二:注解方式
@RestController
@CrossOrigin(origins = "*") // 整个 Controller 都允许跨域
public class UserController {
@GetMapping("/list")
@CrossOrigin(origins = "http://localhost:8080") // 单个方法也可以加
public Result list() {
return Result.success();
}
}方式三:Filter 方式
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}注意:
allowedOrigins("*")和allowCredentials(true)不能同时用(Spring Boot 2.4+),要用allowedOriginPatterns("*")代替。
3.5.2 数据校验(JSR-303)
第 1 章简单提过,这里再详细讲一下。后端接口一定要校验参数,不能信任前端传过来的任何数据。
常用校验注解:
| 注解 | 说明 |
|---|---|
@NotNull | 不能为 null |
@NotBlank | 不能为 null,也不能是空字符串(去掉空格后) |
@NotEmpty | 不能为 null,集合/数组不能为空 |
@Min | 数字最小值 |
@Max | 数字最大值 |
@Size | 字符串/集合大小范围 |
@Email | 邮箱格式 |
@Pattern | 正则表达式匹配 |
@Valid | 级联校验(嵌套对象) |
示例:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度6-20位")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
}Controller 中使用:
@PostMapping("/add")
public Result add(@Valid @RequestBody UserDTO userDTO) {
// 参数校验通过才会进入方法
return Result.success();
}校验不通过会抛出 MethodArgumentNotValidException,在全局异常处理中捕获:
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(message);
}重要原则:前端校验是为了用户体验,后端校验是为了安全。后端校验绝对不能少!
3.5.3 统一返回结果
前面已经提到过,这里再强调一下规范。
标准结构:
{
"code": 200,
"message": "操作成功",
"data": {}
}常见状态码设计:
| 状态码 | 含义 |
|---|---|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未登录 / Token 过期 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
| 10001 | 业务异常(用户不存在等) |
企业实践:
- 前后端分离项目,统一返回 JSON 格式
- 状态码要和前端约定好,文档化
- 成功和失败的结构要一致,前端好处理
3.5.4 常见 Web 问题排查
| 问题 | 可能原因 | 排查方法 |
|---|---|---|
| 静态资源 404 | 路径不对、目录不对、被拦截器拦了 | 检查文件位置、访问路径、拦截器配置 |
| 跨域报错 | 后端没配跨域、配置不对 | 检查 CORS 配置、看浏览器控制台错误 |
| 文件上传失败 | 大小超限、路径不存在、权限问题 | 检查 multipart 配置、上传目录 |
| 拦截器不生效 | 没注册、路径不匹配、顺序问题 | 检查 addInterceptors 配置 |
| 全局异常不生效 | 没加 @RestControllerAdvice、包没扫描到 | 检查注解、包扫描范围 |
| 配置不生效 | 优先级问题、配置写错了 | 检查配置文件位置、拼写、缩进 |
3.6 本章小结
核心知识点回顾:
Java Web 三大组件注册:
- 三种方式:Spring Bean、RegistrationBean(推荐)、注解扫描
- Servlet、Filter、Listener 各自的作用
- 实际开发中用得不多,但要知道怎么注册
Spring MVC 定制:
- 自动配置了哪些特性
- 静态资源映射(默认位置、自定义映射)
- 视图控制器(无业务逻辑的页面跳转)
- 拦截器(登录校验、权限验证)
- WebMvcConfigurer 常用配置
- 不要随便加 @EnableWebMvc
文件上传:
- MultipartFile 的使用
- 单文件、多文件上传
- 上传配置(大小限制)
- 企业级实践(对象存储、安全校验)
异常处理:
- 默认异常处理原理(四大组件)
- 全局异常处理(@RestControllerAdvice + @ExceptionHandler)
- 自定义业务异常
- 统一返回结果
- 异常处理最佳实践
Web 开发进阶:
- 跨域处理(CORS)
- 数据校验(JSR-303)
- 统一返回结果规范
面试高频考点:
- Spring Boot 怎么注册 Servlet/Filter/Listener
- Spring MVC 自动配置了哪些内容
- 静态资源映射的默认位置
- 拦截器的使用和原理
- 全局异常处理怎么实现
- 跨域怎么解决
给实习生的建议:
- 这一章是 Web 开发的基础,一定要动手敲代码练
- 重点掌握:拦截器、文件上传、全局异常处理、跨域,这些是工作中天天用的
- 统一返回结果、参数校验、异常处理,这三个是项目规范,进公司第一天就要遵守
- 多思考为什么这么设计,而不只是记住怎么写
3.7 动手练习作业
基础必做题(全员必做)
- 三大组件练习
- 自定义一个 Filter,实现所有请求的访问日志打印(请求路径、时间)
- 用
FilterRegistrationBean注册,设置执行顺序,排除静态资源路径
- 拦截器练习
- 编写登录拦截器,拦截
/admin/**路径的请求 - 未登录跳转到登录页,已登录放行
- 排除登录页、静态资源路径
- 测试验证:直接访问后台路径被拦截,登录后可正常访问
- 编写登录拦截器,拦截
- 文件上传功能
- 实现单文件上传接口,限制图片类型,大小 5MB 以内
- 文件保存到项目外部目录,UUID 重命名
- 返回文件访问路径,配置静态资源映射可直接浏览器访问
- 测试上传不同大小、不同类型文件,验证校验效果
- 全局异常处理
- 编写全局异常处理器
GlobalExceptionHandler - 自定义业务异常类和错误码枚举
- 分别处理业务异常、参数校验异常、系统异常
- 所有异常统一返回
Result格式
- 编写全局异常处理器
- 参数校验练习
- 编写用户注册 DTO,添加用户名、密码、邮箱、年龄校验
- Controller 方法添加
@Valid触发校验 - 测试非法参数,验证全局异常能否正确返回校验信息
进阶选做题(学有余力完成)
- 多文件上传:实现批量文件上传,返回所有文件的访问路径列表
- 文件下载:实现文件下载接口,支持中文文件名,测试下载正常
- 多拦截器:再添加一个权限拦截器,验证管理员角色,设置执行顺序,登录拦截器先执行,权限拦截器后执行
- 异常日志记录:集成 SLF4J 日志,系统异常记录 error 级别日志,包含异常栈信息
思考题
- Filter 和拦截器有什么区别?分别在什么场景使用?
- 为什么加了
@EnableWebMvc之后静态资源访问不了了? - 文件上传有哪些安全风险?应该怎么防护?
- 全局异常处理的原理是什么?为什么能拦截所有 Controller 的异常?
- 前后端分离项目为什么会有跨域问题?有哪些解决方案?